package net.ermannofranco.xml;

//import java.util.Collection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Tag Fornisce metodi factory per istanziare ed aggiungere i tag. Fornisce
 * metodi per recuperare i tag contenuti, come una Collection. La classe non e'
 * istanziabile direttamente, ma solo tramite i metodi factory definiti in Doc e
 * simili. (ermanno.franco@gmail.com)
 */
public class Tag extends CommentNode implements AbstractTag {

	private static final long serialVersionUID = 5040981385737231814L;
	protected static final String CDATA_START = "<![CDATA[";
	protected static final String CDATA_END = "]]>";
	protected String localNS;// accessibile alle sottoclassi
	private AbstractTag contenitore;
	private String xmlNome;
	private final ArrayList<Node> allElems = new ArrayList<Node>();
	private final ArrayList<ITag> elementi = new ArrayList<ITag>();
	private final ArrayList<TextNode> textBlocks = new ArrayList<TextNode>();
	private Map<String, String> attributi;
	private String comment;

	protected final transient Log log = LogFactory.getLog(getClass());

	/**
	 * costruttore protetto
	 * 
	 * @param xmlNome
	 * @param level
	 * @param contenitore
	 * @throws SyntaxException
	 */
	protected Tag(String xmlNome, int level, AbstractTag contenitore)
			throws SyntaxException {
		super(null, level);
		if (!Static.isNmToken(xmlNome)) {
			throw new SyntaxException(xmlNome + " non valido in xml");
		}
		this.xmlNome = xmlNome;
		if (contenitore != null) {
			this.contenitore = contenitore;
			inheritLocalNS(contenitore);
		}
	}

	// permette di aggiungere oggetti provenienti dalle sottoclassi
	/**
	 * @param o
	 * @return
	 */
	protected boolean add(ITag o) {
		return add(o, false);
	}

	protected boolean add(ITag o, boolean first) {
		return addElem(o, first);
	}

	/**
	 * @param o
	 * @return
	 */
	private boolean addElem(ITag o, boolean first) {
		if (first) {
			allElems.add(0, o);
			elementi.add(0, o);
			return true;
		} else {
			allElems.add(o);
			return elementi.add(o);
		}
	}

	// l'unico modo di creare tag e' qui...
	/**
	 * metodo che crea un tag e contestualmente lo aggiunge alla collezione
	 * degli elementi
	 * 
	 * @param nome
	 *            nome del tag
	 * @throws SyntaxException
	 *             se il nome non e' corretto
	 * @return oggetto tag
	 */
	public Tag addTag(String nome) throws SyntaxException {
		Static.validateNMToken(nome);
		Tag newTag = new Tag(nome, level + 1, this);
		addElem(newTag, false);
		return newTag;
	}

	/**
	 * aggiunge testo senza controllo, incluso in <[CDATA[ ]]>
	 * 
	 * @param text
	 *            blocco di testo
	 * @return autoref ad AbstractTag
	 */
	public final AbstractTag addCDATA(String cdata) {
		TextNode node = new TextNode(CDATA_START + cdata + CDATA_END);
		addText(node);
		return this;
	}

	/**
	 * @param node
	 */
	private void addText(TextNode node) {
		allElems.add(node);
		textBlocks.add(node);
	}

	protected boolean addNode(Node n) {
		return allElems.add(n);
	}

	/**
	 * aggiunge testo filtrato per renderlo leggibile dal parser
	 * 
	 * @param text
	 *            blocco di testo
	 * @return autoref ad AbstractTag
	 */
	public AbstractTag addTextBlock(String text) {
		TextNode node = new TextNode(Static.toXmlString(text));
		addText(node);
		return this;
	}

	/**
	 * permette di sapere se il contenitore non ha altri tag annidati
	 * 
	 * @return se il contenitore non ha tags annidati, ma può avere del testo.
	 */
	public final boolean isLeaf() {
		return elementi.isEmpty();
	}

	public final boolean hasText() {
		return !textBlocks.isEmpty();
	}

	/**
	 * @return
	 */
	protected AbstractTag getContainer() {
		return contenitore;
	}

	/**
	 * restituisce il documento principale
	 * 
	 * @return il documento principale
	 */
	public final Doc getDocument() {
		// Doc estende Tag, usa questo metodo perche' final
		// in Doc il container e' null, e' l'unico.
		if (getContainer() == null) {
			return (Doc) this;
		}
		if (getContainer().getClass().equals(Doc.class)) {
			return (Doc) getContainer();
		}
		// ricorsivo, risale l'albero finche' trova
		return getContainer().getDocument();
	}

	/*
	 * //chissa' se mi servono i metodi di Collection? public final void clear()
	 * { elementi.clear(); }
	 * 
	 * public boolean removeAll(java.util.Collection c) { return
	 * elementi.removeAll(c); }
	 * 
	 * public int hashCode() { int retValue;
	 * 
	 * retValue = super.hashCode(); return retValue; }
	 * 
	 * public boolean addAll(java.util.Collection c) { return
	 * elementi.addAll(c); }
	 * 
	 * public boolean containsAll(java.util.Collection c) { return
	 * elementi.containsAll(c); }
	 * 
	 * public boolean add(Object o) { return elementi.add(o); }
	 * 
	 * public boolean retainAll(java.util.Collection c) { return
	 * elementi.retainAll(c); }
	 * 
	 * public Object[] toArray() { return elementi.toArray(); }
	 * 
	 * public Object[] toArray(Object[] a) { return elementi.toArray(a); }
	 * 
	 * public boolean remove(Object o) { return elementi.remove(o); }
	 * 
	 * public Iterator iterator() { return elementi.iterator(); }
	 * 
	 * public boolean equals(Object o) { boolean retValue;
	 * 
	 * retValue = super.equals(o); return retValue; }
	 * 
	 * public boolean contains(Object o) { return elementi.contains(o); }
	 * 
	 * public int size() { return elementi.size(); }
	 */
	/**
	 * @param nome
	 * @param valore
	 * @return
	 */
	protected AbstractTag addAttr(String nome, String valore) {
		return addAttribute(nome, valore);
	}

	/**
	 * 
	 * @param a
	 *            attributo del tag.
	 * @return autoref
	 */
	public Tag addAttribute(TagAttribute a) {
		return addAttribute(a.getName(), a.getValue());
	}

	/**
	 * aggiunge un attributo al tag
	 * 
	 * @param nome
	 *            nome dell'attributo
	 * @param valore
	 *            valore dell'attributo
	 * @return autoref
	 */
	public Tag addAttribute(String nome, String valore) {
		if (valore == null || nome == null) {
			return this;
		}
		Static.validateNMToken(nome);
		if (attributi == null) {
			attributi = new HashMap<String, String>();
		}
		attributi.put(nome, valore);
		return this;
	}

	/**
	 * @return
	 * @param nome
	 */
	public String getAttribute(String nome) {
		if (attributi == null) {
			return null;
		}
		return attributi.get(nome);
	}

	/**
	 * @return
	 */
	public Enumeration<String> getAttributes() {
		if (attributi == null) {
			return null;
		}
		return new XmlEnumeration(attributi);
	}

	/**
	 * @return
	 */
	public String getName() {
		return xmlNome;

	}

	/**
	 * @param nome
	 * @return
	 */
	public String removeAttribute(String nome) {
		if (attributi == null) {
			return null;
		}
		return (String) attributi.remove(nome);
	}

	protected final String toOpenTagSave() {
		Enumeration<String> en;
		// System.out.println("indent: "+indent);

		StringBuilder sb = new StringBuilder();
		Static.indent(sb, saveIndent, level);

		sb.append("<").append(
				Static.isMinimalStr(localNS) ? "" : localNS + Static.COLON)
				.append(xmlNome);

		if (getAttributes() != null) {
			en = getAttributes();
			while (en.hasMoreElements()) {
				sb.append(" " + en.nextElement());
			}
		}

		return sb.toString();

	}

	/**
	 * @return
	 */
	protected final String toOpenTagPrint() {
		Enumeration<String> en;
		// System.out.println("indent: "+indent);

		StringBuilder sb = new StringBuilder();
		Static.indent(sb, printIndent, level);
		sb.append("<").append(
				Static.isMinimalStr(localNS) ? "" : localNS + Static.COLON)
				.append(xmlNome);

		if (getAttributes() != null) {
			en = getAttributes();
			while (en.hasMoreElements()) {
				sb.append(" " + en.nextElement());
			}
		}

		return sb.toString();

	}

	public String toSave() {
		StringBuilder sb = new StringBuilder();
		if (comment != null) {
			Static.indent(sb, saveIndent, level);
			sb.append(Static.OPEN_COMMENT).append(Static.toXmlString(comment))
					.append(Static.CLOSE_COMMENT);
		}
		sb.append(toOpenTagSave());

		if (isLeaf() && !hasText()) {
			sb.append("/>");
			return sb.toString();
		}

		sb.append(">");

		Iterator<Node> e = allElems.iterator();
		while (e.hasNext()) {
			sb.append(e.next().toSave());
		}
		// indentazione di chiusura, MAI comunque per i tag di chiusura foglie,
		// li mettiamo inline
		if (!isLeaf()) {
			Static.indent(sb, saveIndent, level);
		}

		sb.append("</").append(
				Static.isMinimalStr(localNS) ? "" : localNS + Static.COLON)
				.append(xmlNome).append(">");

		return sb.toString();
	}

	/**
	 * @return stringa per la stampa.
	 */
	public String toPrint() {

		StringBuilder sb = new StringBuilder();
		switch (blankLine) {
		case FIRST_LEVEL:
			if (level == 1)
				sb.append(Static.CRLF);
			break;
		case ALL_LEVELS:
			sb.append(Static.CRLF);
			break;
		default:
			break;
		}
		if (comment != null) {
			Static.indent(sb, printIndent, level);
			sb.append(Static.OPEN_COMMENT).append(Static.toXmlString(comment))
					.append(Static.CLOSE_COMMENT);
		}

		sb.append(toOpenTagPrint());

		if (isLeaf() && !hasText()) {
			sb.append("/>");
			return sb.toString();
		}

		sb.append(">");

		Iterator<Node> e = allElems.iterator();
		while (e.hasNext()) {
			sb.append(e.next().toPrint());
		}

		// indentazione di chiusura, MAI comunque per i tag di chiusura foglie,
		// li mettiamo inline
		if (!isLeaf()) {
			Static.indent(sb, printIndent, level);
		}

		sb.append("</").append(
				Static.isMinimalStr(localNS) ? "" : (localNS + Static.COLON))
				.append(xmlNome).append(">");

		return sb.toString();
	}

	/**
	 * @param comment
	 * @return
	 */
	public AbstractTag setComment(String comment) {
		this.comment = comment;
		return this;

	}

	/**
	 * @param nome
	 * @param uri
	 * @return
	 */
	public AbstractTag addNS(Namespace ns) {

		return addAttr("xmlns"
				+ (Static.isMinimalStr(ns.getPrefix()) ? "" : ":"
						+ ns.getPrefix()), ns.getUri());

	}

	/**
	 * @param nome
	 * @param uri
	 * @return
	 */
	public AbstractTag setLocalNS(Namespace ns) {
		// sostituisce l'eventuale ns;
		localNS = ns.getPrefix();
		return addNS(ns);
	}

	/*
	 * @return
	 */
	public String getLocalNS() {
		return localNS;
	}

	/**
	 * @param container
	 */
	private void inheritLocalNS(AbstractTag container) {
		localNS = container.getLocalNS();
	}

	/**
	 * @return
	 */
	public int getLevel() {
		return level;
	}

	
	@Override
	protected void setBlankLine(BlankLine blankLine) {
		super.setBlankLine(blankLine);
		for (Node n : allElems) {
			if (CommentNode.class.isAssignableFrom(n.getClass())) {
				CommentNode in = (CommentNode) n;
				in.setBlankLine(blankLine);
			}
		}
	}

	public void setPrintIndent(Indent indent) {
		super.setPrintIndent(indent);
		for (Node n : allElems) {
			if (IndentNode.class.isAssignableFrom(n.getClass())) {
				IndentNode in = (IndentNode) n;
				in.setPrintIndent(indent);
			}
		}
	}

	public void setSaveIndent(Indent indent) {
		super.setSaveIndent(indent);
		for (Node n : allElems) {
			if (IndentNode.class.isAssignableFrom(n.getClass())) {
				IndentNode in = (IndentNode) n;
				in.setSaveIndent(indent);
			}
		}
	}

	@Override
	public String toString() {
		return toPrint();
	}

}
